package model.management;

import java.util.*;

import model.entity.INode;
import model.entity.container.ProductContainer;
import model.entity.container.ProductGroup;
import model.entity.container.StorageUnit;
import model.events.EntityEvent;
import model.events.EntityEventEmitter;
import model.events.EntityModifiedEvent;
import persistence.PersistenceMode;
import persistence.ProductContainersDAO;
import reports.visitor.IVisitor;
import common.Result;
import common.ValueResult;

/**
 * Track the instance of the list of storage units
 * 
 * @author Ian Ryu, Ryan Cheatham
 * 
 *         {@invariant storageUnits.size() >= 0}
 */
public class StorageUnitManager extends EntityEventEmitter implements INode
{
  private static StorageUnitManager INSTANCE = new StorageUnitManager();

  public static StorageUnitManager getInstance()
  {
    return INSTANCE;
  }

  private Set<StorageUnit> storageUnits = new HashSet<StorageUnit>();

  private StorageUnitManager()
  {
    _load();
  }

  /**
   * Validates whether the given storage unit can be added to the database.
   * 
   * @param unit
   *          The storage unit to validate.
   * 
   * @return a result, whose status indicates whether the given storage unit can be added to the
   *         database.
   * 
   *         {@pre unit != null} {@post retval != null}
   */
  public static Result validateAdd(StorageUnit unit)
  {
    return INSTANCE._validateAdd(unit);
  }

  // @formatter:off
  /**
   * Attempts to add the given storage unit is added to the database.
   * 
   * @param unit
   *          The unit to be added to the database.
   * 
   * @return a result, indicating whether this operation was successful or not.
   * 
   *         {@pre unit != null}
   * 
   *         {@post unit is in the database} {@post retval != null}
   */
  // @formatter:on
  public static Result add(StorageUnit unit)
  {
    return INSTANCE._add(unit);
  }

  // @formatter:off
  /**
   * Validates whether a StorageUnit can be removed from the database.
   * 
   * @param unit
   *          The storage unit to validate the removal of.
   * @return a result, indicating whether the StorageUnit can be removed or not.
   * 
   *         {@pre unit != null}
   * 
   *         {@post calling {@link #remove(StorageUnit)} will succeed} {@post retval != null}
   */
  // @formatter:on
  public static Result validateRemove(StorageUnit unit)
  {
    return INSTANCE._validateRemove(unit);
  }

  // @formatter:off
  /**
   * Attempts to remove the given storage unit from the database.
   * 
   * @param unit
   *          The unit to be removed from the database.
   * 
   * @return a result, indicating whether this operation was successful or not.
   * 
   *         {@pre unit != null}
   * 
   *         {@post the unit is no longer in the database} {@post retval != null}
   */
  // @formatter:on
  public static Result remove(StorageUnit unit)
  {
    return INSTANCE._remove(unit);
  }

  // @formatter:off
  /**
   * Validates whether the given storage unit can be updated to the new storage unit.
   * 
   * @param before
   *          The storage unit before the update.
   * @param after
   *          The storage unit after the update.
   * 
   * @return a result, whose status indicates whether the given storage unit can be updated.
   * 
   *         {@pre before != null} {@pre after != null}
   * 
   *         {@post retval != null}
   */
  // @formatter:on
  public static Result validateUpdate(StorageUnit before, StorageUnit after)
  {
    return INSTANCE._validateUpdate(before, after);
  }

  // @formatter:off
  /**
   * Attempts to update the given storage unit.
   * 
   * @param before
   *          The storage unit before the update.
   * @param after
   *          The storage unit after the update.
   * 
   * @return a result, indicating whether this operation was successful or not.
   * 
   *         {@pre before != null} {@pre after != null}
   * 
   *         {@post the storage unit is updated in the database} {@post retval != null}
   */
  // @formatter:on
  public static Result update(StorageUnit before, StorageUnit after)
  {
    return INSTANCE._update(before, after);
  }

  /**
   * Clears all StorageUnit data
   */
  public static void clear()
  {
    INSTANCE.storageUnits.clear();
  }

  /**
   * @return An iterator of all <code>Product</code>s in the system.
   * 
   *         {@post retval != null}
   */
  public static Iterator<StorageUnit> getStorageUnits()
  {
    return INSTANCE._getStorageUnits();
  }

  /**
   * Gets the storage unit that contains the given ProductContainer.
   * 
   * @param container
   *          the container in the desired storage unit
   * @return the storage unit that contains the given container.
   */
  public static StorageUnit getStorageUnit(ProductContainer container)
  {
    return INSTANCE._getStorageUnit(container);
  }

  private Result _validateAdd(StorageUnit unit)
  {
    if (storageUnits.contains(unit))
      return new Result(false, "Cannot add StorageUnit - one already exists with the same name!");
    else
      return new Result(true);
  }

  private Result _add(StorageUnit unit)
  {
    Result result = _validateAdd(unit);
    if (result.getStatus() == false)
      return result;
    else
    {
      storageUnits.add(unit);
      save();
      ProductContainersDAO dao = PersistenceMode.getSystemMode().getProductContainersDAO();
      dao.add(unit);
      fireAddedEvent(generateStorageUnitAddedEvent(unit));
      return result;
    }
  }

  private Result _validateRemove(StorageUnit unit)
  {
    Iterator<ProductGroup> groups = unit.getProductGroups();
    while (groups.hasNext())
    {
      ProductGroup group = groups.next();
      Result result = ProductGroupManager.validateRemoveProductGroup(group);
      if (result.getStatus() == false)
        return new Result(false,
            "Cannot remove StorageUnit - cannot remove a child product group!");
    }

    if (unit.getItems().hasNext()) // unit has items
      return new Result(false, "Cannot remove StorageUnit - unit contains items!");

    return new Result(true);
  }

  private Result _remove(StorageUnit unit)
  {
    Result result = _validateRemove(unit);
    if (result.getStatus() == false)
      return result;
    else
    {
      storageUnits.remove(unit);
      ProductContainersDAO dao = PersistenceMode.getSystemMode().getProductContainersDAO();
      dao.delete(unit);
      EntityEvent event = generateStorageUnitRemovedEvent(unit);
      fireRemovedEvent(event);

      return new Result(true);
    }
  }

  private Result _validateUpdate(StorageUnit before, StorageUnit after)
  {
    if (before.equals(after))
      return new Result(true, "Pointless update - the before and after units are equal!");
    if (!storageUnits.contains(before))
      return new Result(false, "Cannot update StorageUnit - before instance does not exist!");
    if (storageUnits.contains(after))
      return new Result(false, "Cannot update StorageUnit - after instance already exists!");
    if (!ProductContainer.isValidName(after.getName()))
      return new Result(false, "Cannot update StorageUnit - after name is not valid!");

    return new Result(true);
  }

  private Result _update(StorageUnit before, StorageUnit after)
  {
    Result result = validateUpdate(before, after);
    if (result.getStatus() == false)
      return result;
    else
    {
      storageUnits.remove(before);
      storageUnits.add(after);

      ItemManager.updateUnit(before, after);
      ProductManager.updateUnit(before, after);
      after.setId(before.getId());
      ProductContainersDAO dao = PersistenceMode.getSystemMode().getProductContainersDAO();
      dao.update(after);

      EntityModifiedEvent event = generateStorageUnitModifiedEvent(before, after);
      fireModifiedEvent(event);

      return new Result(true);
    }
  }

  private EntityEvent generateStorageUnitAddedEvent(StorageUnit storageunit)
  {
    return new EntityEvent(storageunit, EntityEvent.ENTITY_ADDED, null, null);
  }

  private EntityEvent generateStorageUnitRemovedEvent(StorageUnit storageunit)
  {
    return new EntityEvent(storageunit, EntityEvent.ENTITY_REMOVED, null, null);
  }

  private EntityModifiedEvent generateStorageUnitModifiedEvent(ProductContainer before,
      ProductContainer after)
  {
    return new EntityModifiedEvent(before, after);
  }

  private Iterator<StorageUnit> _getStorageUnits()
  {
    List<StorageUnit> units = new LinkedList<StorageUnit>(storageUnits);
    Collections.sort(units);
    return Collections.unmodifiableList(units).iterator();
  }

  private StorageUnit _getStorageUnit(ProductContainer container)
  {
    return container.getStorageUnit();
  }

  public static void save()
  {
    ProductContainersDAO dao = PersistenceMode.getSystemMode().getProductContainersDAO();
    dao.saveStorageUnits(INSTANCE.storageUnits);
  }

  private void _load()
  {
    ProductContainersDAO dao = PersistenceMode.getSystemMode().getProductContainersDAO();
    ValueResult<Set<StorageUnit>> result = dao.getStorageUnits();
    if (!result.getStatus())
    {
      System.err.println("Error loading storage units: " + result.getMessage());
      return;
    }
    ProductGroupManager.productGroups = getAllProductGroups();
    storageUnits = result.getValue();
  }

  public static void load()
  {
    INSTANCE._load();
  }

  @Override
  public String toString()
  {
    String _s = "";
    for (StorageUnit s : storageUnits)
    {
      _s += s.getName() + "\n";
    }
    return _s;
  }

  @Override
  public void preOrderAccept(IVisitor visitor)
  {
    for (StorageUnit unit : storageUnits)
      unit.preOrderAccept(visitor);
  }

  @Override
  public void postOrderAccept(IVisitor visitor)
  {
    for (StorageUnit unit : storageUnits)
      unit.postOrderAccept(visitor);
  }

  public List<ProductGroup> getAllProductGroups()
  {
    List<ProductGroup> result = new ArrayList<ProductGroup>();
    for (StorageUnit unit : storageUnits)
    {
      result.addAll(reGetAllProdcutGroups(unit));
    }
    return result;
  }

  private List<ProductGroup> reGetAllProdcutGroups(ProductContainer pc)
  {
    List<ProductGroup> result = new ArrayList<ProductGroup>();
    if (!pc.getProductGroups().hasNext())
    {
      return result;
    }
    Iterator<ProductGroup> itr = pc.getProductGroups();
    while (itr.hasNext())
    {
      ProductGroup pg = itr.next();
      result.addAll(reGetAllProdcutGroups(pg));
    }
    return result;
  }

  public ProductContainer getContainer(int id)
  {
    ProductContainer result = null;
    for (StorageUnit unit : storageUnits)
    {
      if (unit.getId() == id)
      {
        return unit;
      }
      result = reGetContainer(unit, id);
    }
    return result;
  }

  private ProductContainer reGetContainer(ProductContainer pc, int id)
  {
    ProductContainer result = null;
    if (!pc.getProductGroups().hasNext())
    {
      return result;
    }
    Iterator<ProductGroup> itr = pc.getProductGroups();
    while (itr.hasNext())
    {
      ProductGroup pg = itr.next();
      if (pg.getId() == id)
      {
        return pg;
      }
      result = reGetContainer(pg, id);
    }
    return result;
  }
}
